package distributedbank.gui;

import java.awt.BorderLayout;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.SwingConstants;

import distributedbank.Wrapper;
import distributedbank.gui.messages.Response;
import distributedbank.server.messages.Acknowledgment;
import distributedbank.server.messages.Deposit;
import distributedbank.server.messages.Query;
import distributedbank.server.messages.Transfer;

public class BranchGui
        extends JFrame {

    private static final long serialVersionUID = 1L;

    // int[branch][server], four branches, each with at most three servers
    private final List<Set<Integer>> availableServers = new ArrayList<Set<Integer>>(
            4);

    /**
     * USAGE: java distributedbank.gui.BranchGui BranchID
     * 
     * Runs the BankBranch gui for the branch whose ID is the first argument
     * specified on the command-line
     * 
     * @throws ClassNotFoundException
     */
    public static void main(String[] args) throws IOException,
            ClassNotFoundException {
        if (args.length != 1) {
            System.out
                    .println("Error: You must specify the branch ID on the command-line.");
            System.exit(1);
        } else {
            int branchId = Integer.valueOf(args[0]);
            BranchGui gui = new BranchGui(branchId);

            // not used anymore; we do reliable communication, now
            // GuiListener listener = new GuiListener(gui);
            // listener.start();
        }
    }

    private final int branchId;
    private final JTextField amountField;
    private final JTextField localAccountField;
    private final JTextField remoteAccountField;
    private final JTextField transactionIdField;
    private final JTextArea messageArea;

    private final JButton depositButton;
    private final JButton withdrawButton;
    private final JButton queryButton;
    private final JButton transferButton;
    private final JPanel panel; // the panel that all the text fields live in
    private final Wrapper wrapper;
    private int lastTransactionId;

    // private final Semaphore printLock = new Semaphore(1);

    // private final Semaphore blockLock = new Semaphore(1);

    /**
     * Sets up the GUI and initializes all graphics & text fields.
     */
    public BranchGui(int branchId) throws IOException, ClassNotFoundException {
        super("Branch " + branchId);
        // initialize available servers
        // TODO:
        availableServers.add(0, null);

        this.wrapper = new Wrapper(branchId, 0, true);
        final BranchGui branchGui = this; // for use later on in inner classes

        this.branchId = branchId;
        this.lastTransactionId = branchId;

        depositButton = new JButton("Deposit");
        withdrawButton = new JButton("Withdraw");
        queryButton = new JButton("Query");
        transferButton = new JButton("Transfer");

        // contains the buttons & input boxes
        this.panel = new JPanel();
        panel.setLayout(new GridLayout(4, 3, 2, 2));

        // row 1
        panel.add(new JLabel("Amount ($):", SwingConstants.RIGHT));
        this.amountField = new JTextField();
        panel.add(this.amountField);
        panel.add(depositButton);

        // row 2
        panel.add(new JLabel("Account #:  "
                + String.format("%02d", this.branchId) + ".",
                SwingConstants.RIGHT));
        this.localAccountField = new JTextField(5);
        panel.add(this.localAccountField);
        panel.add(withdrawButton);

        // row 3
        panel.add(new JLabel("Transfer to:", SwingConstants.RIGHT));
        this.remoteAccountField = new JTextField(7);
        panel.add(this.remoteAccountField);
        panel.add(queryButton);

        // row 4
        panel.add(new JLabel("Transaction # (optional):", SwingConstants.RIGHT));
        this.transactionIdField = new JTextField();
        panel.add(this.transactionIdField);
        panel.add(transferButton);

        ActionListener listener = new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                try {
                    Object button = e.getSource();
                    if (button == depositButton) {
                        branchGui.deposit(false);
                    } else if (button == withdrawButton) {
                        branchGui.deposit(true);
                    } else if (button == queryButton) {
                        branchGui.query();
                    } else if (button == transferButton) {
                        branchGui.transfer();
                    }
                } catch (Exception err) {
                    branchGui.println("\nStack Trace:");
                    for (Object x : err.getStackTrace()) {
                        branchGui.println(x.toString());
                    }
                    branchGui.println(err.toString());
                    // branchGui.blockGUI(false);
                }
            }
        };
        for (JButton button : new JButton[] { depositButton, withdrawButton,
                queryButton, transferButton }) {
            button.addActionListener(listener);
        }

        this.add(panel, BorderLayout.NORTH);
        this.messageArea = new JTextArea(10, 25);
        this.messageArea.setEditable(false);
        JScrollPane scrollPane = new JScrollPane(this.messageArea);
        scrollPane
                .setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
        scrollPane
                .setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
        this.add(scrollPane, BorderLayout.CENTER);
        this.println("Welcome to Branch " + this.branchId);

        this.pack();
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        this.wrapper.waitForInitialOracleNotification();
        this.setVisible(true);

    }

    /**
     * Returns the transaction id that is entered in the gui. If the transaction
     * id is invalid, an error is displayed and null is returned. If no
     * transaction id is specified in the gui, a transaction id is
     * auto-generated.
     */
    private Integer getTransactionId() {
        String transactionString = this.transactionIdField.getText().trim();
        int transactionId;
        if (!transactionString.matches("^[0-9]*$")) {
            this.println("-- The transaction id must contain only digits!");
            return null;
        }
        if (!transactionString.isEmpty()) {
            transactionId = Integer.valueOf(transactionString);
            if (transactionId % 100 != this.branchId) {
                this.println("-- The last two digits of the transaction id must match the branch id!");
                return null;
            }
        } else {
            transactionId = this.lastTransactionId + 100;
        }

        this.lastTransactionId = Math
                .max(transactionId, this.lastTransactionId);

        return transactionId;
    }

    /**
     * Returns the account id specified by the user. if the account id is
     * invalid, null is returned.
     */
    private Integer getLocalAccountId() {
        String accountString = this.localAccountField.getText().trim();
        if (accountString.isEmpty()) {
            this.println("-- You must enter an account id to query!");
            return null;
        }
        if (accountString.length() > 5) {
            this.println("-- The account id must be no longer than 5 digits!");
            return null;
        }
        if (!accountString.matches("^[0-9]*$")) {
            this.println("-- The local account number must contain only digits!");
            return null;
        }
        return Integer.valueOf(accountString);
    }

    /**
     * Returns the amount entered by the user in the gui. If the amount is not a
     * valid number, null is returned.
     */
    private Integer getAmount() {
        String amountString = this.amountField.getText().trim();
        if (amountString.isEmpty()) {
            this.println("-- You must enter an amount!");
            return null;
        }
        float amount;
        try {
            amount = Float.valueOf(amountString);
        } catch (NumberFormatException e) {
            this.println("-- The amount you entered is not a valid number!");
            return null;
        }

        return Math.round(100 * amount);
    }

    /**
     * Returns the remote account id entered by the user. If the account id is
     * invalid, null is returned.
     */
    private Integer getRemoteAccountId() {
        String accountString = this.remoteAccountField.getText().trim();
        if (accountString.isEmpty()) {
            this.println("-- You must enter the account to which you want to transfer money!");
            return null;
        }
        if (!accountString.matches("^[0-9]{2}\\.[0-9]{5}$")) {
            this.println("-- The destination account id must be in the form '01.12345'!");
            return null;
        }

        return Integer.valueOf(accountString.substring(3));
    }

    /**
     * Returns the destination branch entered by the user. Prerequisite: the
     * account id must previously have been verified to be valid.
     */
    private Integer getRemoteBranchId() {
        return Integer.valueOf(this.remoteAccountField.getText().trim()
                .substring(0, 2));
    }

    /**
     * This method is called when the "transfer" button is pressed. It sends a
     * transfer request to the appropriate BankBranch server.
     */
    private void transfer() throws IOException, ClassNotFoundException {
        Integer accountId = this.getLocalAccountId();
        Integer transactionId = this.getTransactionId();
        Integer amount = this.getAmount();
        Integer destAccountId = this.getRemoteAccountId();

        if (transactionId != null && accountId != null && amount != null
                && destAccountId != null) {
            // this.blockGUI(true);
            int destBranchId = this.getRemoteBranchId();
            // gets the server id
            // int destServerId = this.getPrimaryServerByBranchId(destBranchId);
            Acknowledgment ack = wrapper.sendReliable(new Transfer(
                    this.branchId, accountId, transactionId, wrapper.localPort,
                    destBranchId, destAccountId, amount));
            ((Response) ack).runResponse(this);
        }
    }

    /**
     * This method is called when the "deposit" or "withdraw" buttons are
     * pressed. The "negative" argument should be set to true if the "withdraw"
     * button was pressed, false otherwise.
     */
    private void deposit(boolean negative) throws IOException,
            ClassNotFoundException {
        Integer accountId = this.getLocalAccountId();
        Integer transactionId = this.getTransactionId();
        Integer amount = this.getAmount();

        if (transactionId != null && accountId != null && amount != null) {
            if (negative) {
                amount = -amount;
            }
            // this.blockGUI(true);
            Acknowledgment ack = wrapper.sendReliable(new Deposit(
                    this.branchId, accountId, transactionId, wrapper.localPort,
                    amount));
            ((Response) ack).runResponse(this);
        }
    }

    /**
     * This method is called when the "query" button is pressed. It sends a
     * query message to the appropriate BankBranch Server.
     */
    private void query() throws IOException, ClassNotFoundException {
        Integer accountId = this.getLocalAccountId();
        Integer transactionId = this.getTransactionId();

        if (transactionId != null && accountId != null) {
            // this.blockGUI(true);
            Acknowledgment ack = wrapper.sendReliable(new Query(this.branchId,
                    accountId, transactionId, wrapper.localPort));
            ((Response) ack).runResponse(this);
        }
    }

    /**
     * Blocks the gui from performing any deposit/withdraw/query/transfer if
     * doBlock is true. Otherwise, the gui will be unblocked again.
     */
    // public void blockGUI(boolean doBlock) {
    // try {
    // this.blockLock.acquire();
    // } catch (InterruptedException e) {
    // }

    // for (int i : new int[] { 11, 8, 5, 2 }) {
    // this.panel.remove(i);
    // }

    // if (doBlock) {
    // this.panel.add(new JLabel("Waiting...", SwingConstants.CENTER), 2);
    // for (int i : new int[] { 5, 8, 11 }) {
    // this.panel.add(new JPanel(), i);
    // }
    // } else {
    // this.panel.add(this.depositButton, 2);
    // this.panel.add(this.withdrawButton, 5);
    // this.panel.add(this.queryButton, 8);
    // this.panel.add(this.transferButton, 11);
    // }
    // this.panel.validate();
    // this.panel.repaint();

    // this.blockLock.release();
    // }

    /**
     * Prints a message to the gui's text-area.
     */
    public void println(String message) {
        // single-threaded; no need for a lock!
        // try {
        // this.printLock.acquire();
        // } catch (InterruptedException e) {
        // }

        this.messageArea.append(message + "\n");
        this.messageArea.setCaretPosition(this.messageArea.getText().length());
        // this.printLock.release();
    }
}
